Example:
結果分別為"420"(string)
與42(number)
,會導致這樣的差異,一般的認知會認為,只要+
運算子的其中任一邊運算元為string,那就會進行string的串接。這部分是對的,但實際運作情況可能會比我們所想的還要複雜。
Example:
以上的2個運算元是array,不是string,但運算結果卻是一個string,我們來分析這種情況是怎麼產生的。
依據ES5的規格,若+
運算子的其中一個運算元是object的話,首先會想辦法把object轉成基型值,而array是屬於object,但無法使用valueOf( )
方法產生基型值,所以它會改用toString( )
方法。因此那2個array分別轉成"1,2"
與"3,4"
,串接就變成"1,23,4"
。
所以結論是,若+
運算子的其中一個運算元是string(或是能轉型為string)的話,那就會是進行串接動作,不然的話,就會是數值相加。
根據以上的論述,我們可以把number跟空字串(" ")相加,就能把number轉型成string:
+
運算子的數值加法是具有可交換性的,2+3
與3+2
的結果是一樣的。但作為string串接,就不是了,"a"+"b"
跟"b"+"a"
,結果會是不一樣的string。但以上面的例子來說是具有可交換性的,a + ""
與"" + a
都是讓a
變成string。
使用+
運算子來達到轉型的目的是很常見的手法,但有個細節需要特別注意。
Example:a+""
會直接呼叫valueOf( )
方法,但String( )
會呼叫toString( )
方法,就會造成不同的結果。
一般來說,這種況情況不會造成困擾,但若我們自己定義某個物件的valueOf( )
方法與toString( )
方法,就要特別注意了。
string隱含地強制轉型成number:
-
運算子只能用來做數值的減法,所以a - 0
中的a
勢必會轉成number,a * 1
、a / 1
也會產生相同的結果。
array的轉型:
上面的array會先轉型成string,再轉成number。
b = String( a )
與b = a + ""
,這2者都是合法的語法,但b = a + ""
能見度較高。
Example:
上面的結果,只有在「所有的引數中,只有一個true或truthy」的情況下,才會為true,這顯然不是一個很好的程式碼,可讀性差。若我們要處理的引數更多呢?
這時,我們換另一個方式,利用boolean轉型為number的特性:
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
if (arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}
var a = true;
var b = false;
onlyOne( b, a ); // true
onlyOne( b, a, b, b, b ); // true
onlyOne( b, b ); // false
onlyOne( b, a, b, b, b, a ); // false
關於arguments[i]
可以參考這篇文章 call函式 & arguments物件
如果至少有一個引數的話,就會進入for迴圈。若arguments[i]
的值為true(1),條件成立,sum加1;若第二次(以上)成立,sum會大於1,sum == 1
的結果就會為false。換言之,只要有2個以上(含)的a
,那結果就會為false。
另一個Example:
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
sum += Number( !!arguments[i] );
}
return sum === 1;
}
onlyOne( "42", 0 ) //true
利用!!arguments[i]
會強制把值轉換成true或false,再利用Number( )
會把boolean轉成0或1。
只要2個以上(含)成立,sum就不會是1,自然運算結果就為false。
相信透過上述的強制轉型,可讀性會比一堆 && 跟 || 來的高,並提高了可擴充性。
隱含地強制轉型是由於一個值的運算過程,迫使它必須轉型才發生的事情。
會發生隱含地強制轉型的情況:
var a = 42;
var b = "abc";
var c;
var d = null;
if (a) {
console.log( "yep" ); // yep
}
while (c) {
console.log( "nope, never runs" );
}
c = d ? a : b;
c; // "abc"
if ((a && d) || c) {
console.log( "yep" ); // yep
}
在以上的情境中,非boolean值都會隱含地強制轉型成boolean值,以做出判斷。
||(or) 與 &&(and)運算子,實際上不會產生boolean值,而是2個運算元中的其中一個值。
換句話說,它們會選擇其中一個運算元的值。
Example:
|| 與 && 運算子會在第一個運算元進行boolean(非boolean值會轉型)測試。
以 || 來說,若測試結果為true,那就會回傳第1個運算元的值,反之,回傳第2個。
以 && 來說,若測試結果為true,那就會回傳第2個運算元的值,反之,回傳第1個。
|| 與 && 運算式的值,永遠都會是其中一個運算元的值,而非測試結果(boolean)。
思考另一個例子:a || b
與a ? a : b
的結果雖然相等,卻存在著細微差異。
在a ? a : b
中,若a是一個較複雜的運算式,且運算結果為true的話,那a有可能會被估算2次。
而a || b
,a只會被估算一次,而該值會被用於測試中,若為true,也會被當作結果輸出。
此種方式有個很常見的手法:
function foo(a,b) {
a = a || "hello";
b = b || "world";
console.log( a + " " + b );
}
foo(); // "hello world"
foo( "yeah", "yeah!" ); // "yeah yeah!"
以a
來說,若沒有傳入值,或是falsy值,a
就會是備用的值'"hello"'。
但如果是
foo( "That's it!", "" ); // "That's it! world"
第二個值,即便我們傳入"",依舊是flasy值,就會替換成"world"。
如若要更明確的測試,可以改用三元運算子。
關於&&的範例:
function foo() {
console.log( a );
}
var a = 42;
a && foo(); // 42
只有在a
為true的情況下,才會執行foo( )
方法,所以這種方式有時也會被稱為「守護運算子」或是「短路」。
關於「短路」,會在第5章提到。
上面的方式,有另一種呈現,你或許看過:
if (a) {
foo();
}
不過,JS還是會採用捷徑的方式來處理。
來看看另一種情境:
var a = 42;
var b = null;
var c = "foo";
if (a && (b || c)) {
console.log( "yep" );
}
結果會產生yep。if敘述句判斷式會將a && (b || c)
的結果"foo"
,強制轉型成true。
善用隱含地強制轉型,可以讓我們的程式碼可讀性與維護性更高。
或者,可以試試明確地強制轉型:
if (!!a && (!!b || !!c)) {
console.log( "yep" );
}
哪種方式比較適合,心中自有答案。
此為You Don't Know JS系列的筆記。